/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */

package org.mule.module.db.sql.executor;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.any;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.mule.module.db.internal.domain.autogeneratedkey.NoAutoGeneratedKeyStrategy;
import org.mule.module.db.internal.domain.connection.DbConnection;
import org.mule.module.db.internal.domain.param.DefaultOutputQueryParam;
import org.mule.module.db.internal.domain.param.QueryParam;
import org.mule.module.db.internal.domain.query.QueryTemplate;
import org.mule.module.db.internal.domain.query.QueryType;
import org.mule.module.db.internal.domain.type.UnknownDbType;
import org.mule.module.db.internal.result.resultset.ResultSetHandler;
import org.mule.module.db.internal.result.statement.OutputParamResult;
import org.mule.module.db.internal.result.statement.ResultSetResult;
import org.mule.module.db.internal.result.statement.StatementResult;
import org.mule.module.db.internal.result.statement.StatementResultIterator;
import org.mule.module.db.internal.result.statement.UpdateCountResult;
import org.mule.tck.junit4.AbstractMuleTestCase;
import org.mule.tck.size.SmallTest;

import java.sql.CallableStatement;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;

import org.hamcrest.core.Is;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

@SmallTest
public class StatementResultIteratorTestCase extends AbstractMuleTestCase
{

    private final CallableStatement statement = mock(CallableStatement.class);
    private final QueryTemplate queryTemplate = new QueryTemplate("dummy", QueryType.STORE_PROCEDURE_CALL, Collections.EMPTY_LIST);
    private final DbConnection connection = mock(DbConnection.class);
    private final ResultSetHandler resultSetHandler = mock(ResultSetHandler.class);
    private final StatementResultIterator iterator = new StatementResultIterator(connection, statement, queryTemplate, new NoAutoGeneratedKeyStrategy(), resultSetHandler);

    @Before
    public void configureConnectionMetadata() throws Exception
    {
        DatabaseMetaData metaData = mock(DatabaseMetaData.class);
        when(connection.getMetaData()).thenReturn(metaData);
    }

    @Before
    public void configureResultSetHandler() throws Exception
    {
        when(resultSetHandler.processResultSet(argThat(any(DbConnection.class)), argThat(any(ResultSet.class)))).then(new Answer<Object>()
        {
            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable
            {
                return invocationOnMock.getArguments()[1];
            }
        });
    }

    @Test
    public void detectHasResultSet() throws Exception
    {
        ResultSet resultSet = mock(ResultSet.class);
        when(statement.getResultSet()).thenReturn(resultSet);
        boolean hasNext = iterator.hasNext();

        assertThat(hasNext, equalTo(true));
    }

    @Test
    public void detectHasUpdateCount() throws Exception
    {
        int updateCount = 10;
        when(statement.getUpdateCount()).thenReturn(updateCount);
        boolean hasNext = iterator.hasNext();

        assertThat(hasNext, equalTo(true));
    }

    @Test
    public void detectHasOutputParam() throws Exception
    {
        when(statement.getResultSet()).thenReturn(null);
        when(statement.getUpdateCount()).thenReturn(StatementResultIterator.NO_UPDATE_COUNT);

        DefaultOutputQueryParam param1 = new DefaultOutputQueryParam(0, UnknownDbType.getInstance(), "param1");
        List<QueryParam> params = new LinkedList<QueryParam>();
        params.add(param1);

        QueryTemplate queryTemplate = new QueryTemplate("dummy", QueryType.STORE_PROCEDURE_CALL, params);
        StatementResultIterator iterator = new StatementResultIterator(null, statement, queryTemplate, null, null);

        boolean hasNext = iterator.hasNext();

        assertThat(hasNext, equalTo(true));
    }

    @Test
    public void returnsResultSetInNextResult() throws Exception
    {
        ResultSet resultSet = mock(ResultSet.class);
        when(statement.getResultSet()).thenReturn(resultSet);

        Object nextResult = iterator.next();

        assertThat(nextResult, is(instanceOf(ResultSetResult.class)));
        ResultSetResult resultSetResult = (ResultSetResult) nextResult;
        assertThat(resultSetResult.getName(), equalTo("resultSet1"));
        assertThat((ResultSet) resultSetResult.getResult(), equalTo(resultSet));
    }

    @Test
    public void returnsUpdateCountInNextResult() throws Exception
    {
        int updateCount = 10;
        when(statement.getUpdateCount()).thenReturn(updateCount);

        Object nextResult = iterator.next();

        assertThat(nextResult, is(instanceOf(UpdateCountResult.class)));
        UpdateCountResult updateCountResult = (UpdateCountResult) nextResult;
        assertThat(updateCountResult.getName(), equalTo("updateCount1"));
        assertThat((Integer) updateCountResult.getResult(), equalTo(updateCount));
    }

    @Test
    public void returnsOutputParamInNextResult() throws Exception
    {
        int paramValue = 7;
        when(statement.getResultSet()).thenReturn(null);
        when(statement.getUpdateCount()).thenReturn(StatementResultIterator.NO_UPDATE_COUNT);
        when(statement.getObject(1)).thenReturn(7);

        DefaultOutputQueryParam param1 = new DefaultOutputQueryParam(1, UnknownDbType.getInstance(), "param1");
        List<QueryParam> params = new LinkedList<QueryParam>();
        params.add(param1);

        QueryTemplate queryTemplate = new QueryTemplate("dummy", QueryType.STORE_PROCEDURE_CALL, params);
        StatementResultIterator iterator = new StatementResultIterator(null, statement, queryTemplate, null, null);

        Object nextResult = iterator.next();

        assertThat(nextResult, is(instanceOf(OutputParamResult.class)));
        OutputParamResult outputParamResult = (OutputParamResult) nextResult;
        assertThat(outputParamResult.getName(), equalTo("param1"));
        assertThat((Integer) outputParamResult.getResult(), equalTo(paramValue));
    }

    @Test
    public void processResultSetOnce() throws Exception
    {
        when(statement.getResultSet()).thenReturn(mock(ResultSet.class));

        iterator.next();
        iterator.next();

        InOrder inOrder = inOrder(statement);
        inOrder.verify(statement).getResultSet();
        inOrder.verify(statement).getMoreResults();
    }

    @Test
    public void processUpdateCountOnce() throws Exception
    {
        when(statement.getUpdateCount()).thenReturn(10);

        iterator.next();
        iterator.next();

        InOrder inOrder = inOrder(statement);
        inOrder.verify(statement).getUpdateCount();
        inOrder.verify(statement).getMoreResults();
    }

    @Test
    public void processOutputParamOnce() throws Exception
    {
        when(statement.getResultSet()).thenReturn(null);
        when(statement.getUpdateCount()).thenReturn(StatementResultIterator.NO_UPDATE_COUNT);
        when(statement.getObject(1)).thenReturn(7);

        DefaultOutputQueryParam param1 = new DefaultOutputQueryParam(1, UnknownDbType.getInstance(), "param1");
        DefaultOutputQueryParam param2 = new DefaultOutputQueryParam(2, UnknownDbType.getInstance(), "param2");
        List<QueryParam> params = new LinkedList<QueryParam>();
        params.add(param1);
        params.add(param2);

        QueryTemplate queryTemplate = new QueryTemplate("dummy", QueryType.STORE_PROCEDURE_CALL, params);
        StatementResultIterator iterator = new StatementResultIterator(connection, statement, queryTemplate, new NoAutoGeneratedKeyStrategy(), null);

        iterator.next();
        iterator.next();

        InOrder inOrder = inOrder(statement);
        inOrder.verify(statement).getObject(1);
        inOrder.verify(statement).getObject(2);
    }

    @Test
    public void cachesHasNext() throws Exception
    {
        ResultSet resultSet = mock(ResultSet.class);
        when(statement.getResultSet()).thenReturn(resultSet);
        iterator.hasNext();

        boolean hasNext = iterator.hasNext();

        assertThat(hasNext, equalTo(true));
        verify(statement).getResultSet();
    }

    @Test
    public void clearsCacheOnNextInvocation() throws Exception
    {
        ResultSet resultSet = mock(ResultSet.class);
        when(statement.getResultSet()).thenReturn(resultSet);
        iterator.hasNext();
        iterator.next();

        boolean hasNext = iterator.hasNext();

        assertThat(hasNext, equalTo(true));
        verify(statement, times(2)).getResultSet();
    }


    @Test(expected = NoSuchElementException.class)
    public void throwsExceptionWhenNoElementExist() throws Exception
    {
        when(statement.getUpdateCount()).thenReturn(StatementResultIterator.NO_UPDATE_COUNT);
        when(statement.getResultSet()).thenReturn(null);

        iterator.next();
    }

    @Test
    public void returnsMultipleResultSetsWhenStreamingIsSupported() throws Exception
    {
        mockConnectionMetaData(true);
        when(resultSetHandler.requiresMultipleOpenedResults()).thenReturn(true);

        ResultSet resultSet1 = mock(ResultSet.class);
        ResultSet resultSet2 = mock(ResultSet.class);
        when(statement.getResultSet()).thenReturn(resultSet1).thenReturn(resultSet2);

        StatementResult result1 = iterator.next();
        StatementResult result2 = iterator.next();

        assertThat(result1.getResult(), Is.<Object>is(resultSet1));
        assertThat(result2.getResult(), Is.<Object>is(resultSet2));
    }

    @Test(expected = IllegalStateException.class)
    public void failsAfterGettingResultSetWhenStreamingIsUnsupported() throws Exception
    {
        mockConnectionMetaData(false);
        when(resultSetHandler.requiresMultipleOpenedResults()).thenReturn(true);

        ResultSet resultSet = mock(ResultSet.class);
        when(statement.getResultSet()).thenReturn(resultSet);

        iterator.next();
        iterator.next();
    }

    private void mockConnectionMetaData(Boolean supportsMultipleOpenResults) throws SQLException
    {
        DatabaseMetaData metaData = mock(DatabaseMetaData.class);
        when(metaData.supportsMultipleOpenResults()).thenReturn(supportsMultipleOpenResults);
        when(connection.getMetaData()).thenReturn(metaData);
    }
}
